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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,6 @@ rewrite.yml
website/node_modules/
website/dist/
website/.astro/

# Local tool config, not part of the project
.claude/
24 changes: 20 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,29 @@ All notable changes to ThrillhouseBot.

## [Unreleased]

## [0.6.2] β€” 2026-08-14

Follow-ups to the review threads on 0.6.1, plus the first piece of the release
chain automation. A review GitHub refuses is now diagnosed and preserved, a
review whose PR moved under it stands down for the run that replaces it, and the
decline re-check and injection-sink floor each close a further set of gaps found
by the dogfood corpus. No configuration changes; upgrading is a redeploy.

### Changed

- **The post-release version bump fails loudly** (#11): the release workflow's bump job branched on `gh pr create` succeeding or the PR already existing, and reported success for every other outcome, so a bump lost to a permissions or API error passed silently. It now distinguishes the three cases and fails the job on the third. `docs/RELEASING.md` describes the release flow end to end, including the one-time repository settings the bump depends on

### Fixed

- **A review post GitHub refuses is diagnosed, abandoned when stale, and never lost** (#704): a rejected review post now logs GitHub's own response body (redacted and length-capped) instead of only the status code; a run whose PR head moved while the model call ran abandons its post β€” counted as a structured `HEAD_MOVED` skip, its check run concluded as skipped β€” because the coalesced run for the new head re-reviews and posts in its place; and a summary-only review GitHub definitely refused (a response-carrying 4xx β€” an ambiguous timeout/5xx still fails, since the review may have landed) falls back to posting the same body as an issue comment through the capped, paced write path, instead of discarding the generation behind a "review could not be completed" notice
- **A review post GitHub refuses is diagnosed, abandoned when stale, and never lost** (#704): a rejected review post logs GitHub's own response body, redacted and length-capped, so the cause is visible; previously only the status code reached the logs. A run whose PR head moved during the model call now stands down, recorded as a structured `HEAD_MOVED` skip with its check run concluded as skipped, because the coalesced run for the new head re-reviews and posts in its place. A summary-only review that GitHub definitely refused, meaning a response-carrying 4xx, falls back to posting the same body as an issue comment through the capped and paced write path, so the generation survives. An ambiguous timeout or 5xx still fails, since the review may have landed
- **Inline code spans in a decline are stripped delimiter-aware** (#697): the decline re-check scans backtick runs the CommonMark way, where an opening run of N backticks closes at the next run of exactly N. A span whose body carries a longer backtick run (`` `a``b` ``) or one line ending is now stripped whole, so quoted claim text can no longer reopen a correct decline. An unclosed run stays literal, and a length bound keeps a stray backtick from swallowing the reply
- **Mention-form commands follow the configured bot login** (#698): `TriggerDetector` builds the `@<bot> <command>` trigger patterns from `BotIdentity.mentionNames()`, so `@my-review-bot review` works on an install whose GitHub App runs under a custom login. The mention's `@` must open the comment or follow a non-word character, so an email local part never triggers a command. Slash forms and default-config behaviour are unchanged
- **The injection-sink floor closes four residual defeater gaps** (#696): "Nothing escapes parameterization" and "Nothing escapes; the sanitizer runs on render" are read as the mitigations they assert, so the floor no longer over-fires on them. A do-supported mitigation carrying an adverb ("does always escape") defeats the floor. A modal absence claim ("Nothing can sanitize the value") registers and floors at high. A comma-coordinated asserted mitigation ("Nothing escapes, but the sanitizer runs on render") defeats the floor, while its denial twin ("...but the sanitizer is disabled") still floors

### Dependencies

- **Inline code spans in a decline are stripped delimiter-aware** (#697): the decline re-check now scans backtick runs the CommonMark way β€” an opening run of N backticks closes at the next run of exactly N β€” so a span whose body carries a longer backtick run (`` `a``b` ``) or one line ending is stripped whole instead of leaving quoted claim text to reopen a correct decline. An unclosed run stays literal, and a length bound still keeps a stray backtick from swallowing the reply
- **Mention-form commands follow the configured bot login** (#698): `TriggerDetector` builds the `@<bot> <command>` trigger patterns from `BotIdentity.mentionNames()` instead of a hardcoded slug, so `@my-review-bot review` works on a custom-login install; the mention's `@` must open the comment or follow a non-word character, so an email local part never triggers a command. Slash forms and default-config behavior are unchanged
- **The injection-sink floor closes four residual defeater gaps** (#696): "Nothing escapes parameterization" and "Nothing escapes; the sanitizer runs on render" read as the mitigations they assert instead of over-firing the floor, a do-supported mitigation with an adverb ("does always escape") defeats the floor, a modal absence claim ("Nothing can sanitize the value") now registers and floors at high, and a comma-coordinated asserted mitigation ("Nothing escapes, but the sanitizer runs on render") defeats the floor while its denial twin ("...but the sanitizer is disabled") still floors
- Bumped the Quarkus platform from 3.38.0 to 3.38.1 (#706)
- Bumped `actions/attest-build-provenance` from 4.1.1 to 4.2.2 in the release workflow (#708)

## [0.6.1] β€” 2026-08-13

Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,10 @@ not a reaction.
| `/resume` | Re-enable the bot on a paused PR | write |
| `@thrillhousebot resolved <path>:<line> β€” <title>` | Close a previous finding that has no review thread to reply on, so it stops holding approval (see **Clearing a finding with no thread** under Configuration) | write |

**Access** β€” every command except `/help` requires the commenter to hold write access to
the repository (or to be named in
`THRILLHOUSEBOT_REVIEW_MANUAL_TRIGGER_ALLOWED_LOGINS`), since reviews spend the operator's
AI budget.
**Access** β€” every slash command except `/help` requires the commenter to hold write access
to the repository, or to be named in `THRILLHOUSEBOT_REVIEW_MANUAL_TRIGGER_ALLOWED_LOGINS`,
since reviews spend the operator's AI budget. The allowlist covers the slash commands only:
the `@thrillhousebot resolved` directive always requires write access, as described below.

**`@thrillhousebot resolved`** β€” a directive, not a slash command: it has no `/resolved`
form, and it is read by the *next* review rather than acted on immediately. The bot replies
Expand Down Expand Up @@ -288,7 +288,7 @@ will change per provider:
| `GITHUB_WRITE_MIN_INTERVAL` | Duration spacing two content-creating GitHub calls (comments, review comments, thread replies, reviews), shared process-wide. GitHub secondary-rate-limits rapid content creation and answers `403`; pacing keeps the bot inside that envelope instead of discovering it by rejection β€” its published guidance is no more than one such request per second. `0` disables pacing | `1s` |
| `GITHUB_WRITE_MAX_WAIT` | Duration ceiling on how long one caller waits for its content-creation slot. Past it the call goes out unpaced and the bounded backoff handles a refusal, so a long queue never parks a finished command | `60s` |
| `WEBHOOK_DEDUP_TTL` | Webhook deduplication time-to-live for GitHub redeliveries | `24h` |
| `THRILLHOUSEBOT_REVIEW_MANUAL_TRIGGER_ALLOWED_LOGINS` | Comma-separated allowlist of logins permitted to trigger manual `/review` without repo access | _(empty)_ |
| `THRILLHOUSEBOT_REVIEW_MANUAL_TRIGGER_ALLOWED_LOGINS` | Comma-separated allowlist of logins permitted to run the slash commands without repo access; does not extend to the `@thrillhousebot resolved` directive, which always requires write access | _(empty)_ |
| `MANUAL_TRIGGER_AUTH_TIMEOUT` | Upper bound on the manual-trigger write-access check on the webhook ACK thread; fails closed (denies) if GitHub is slower | `5s` |
| `ACK_REACTION_TIMEOUT` | Upper bound on the πŸ‘€ command-ack reaction on the webhook ACK thread; the wait is abandoned (reaction may land late) if GitHub is slower | `3s` |
| `AUTO_REVIEW_MIN_INTERVAL` | Minimum interval between automatic reviews of the same PR β€” pushes within the window are skipped silently, even on a new head SHA (in-memory, per replica). A manual `/review` always bypasses; unset or `0` reviews every push | `0` (disabled) |
Expand Down
59 changes: 58 additions & 1 deletion docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,64 @@ against an existing tag. The jobs run in order:
5. **bump-version** β€” opens a PR moving `main` to the next `-SNAPSHOT` version.

To cut a release: update `CHANGELOG.md`, set the release version in `pom.xml`,
merge, then tag the merge commit `vX.Y.Z` and push the tag.
freeze the outgoing docs version (below), merge, then tag the merge commit
`vX.Y.Z` and push the tag.

## Freezing the docs version

The version picker on the docs site is driven by `website/versions.json`, and
each archived version is a snapshot under `website/src/content/docs/<slug>/`.
Neither is produced by CI, so **the freeze is a manual step in the release PR**.
Skipping it leaves the site serving the new version's docs under the old
version's label. It has been missed at four cuts so far (0.4.0, 0.5.0, 0.6.0 and
0.6.1), each time caught only at the following release.

Take the snapshot of the version being *replaced*, from that version's own tag,
because `archive-docs-version.mjs` expands the `remarkInclude` markers at archive
time and would otherwise capture the working tree:

```bash
git restore --source vX.Y.Z -- README.md CONTRIBUTING.md docs/ website/src/content/docs/
cd website && npm run docs:archive -- X.Y.Z
git restore --source HEAD -- README.md CONTRIBUTING.md docs/ website/src/content/docs/
```

Then set `current.label` in `website/versions.json` to the version being cut, and
confirm `npm run build` picks up the new pages.

Check `git status` after the second restore: a restore from a tag brings back any
file deleted since that tag, so anything outside the new archive directory and
`versions.json` is a file the release should not be resurrecting.

## Publishing the docs

`.github/workflows/docs.yml` deploys the live site on `release: published` only,
so the site tracks releases rather than `main`. That trigger runs **against the
tag ref**, which makes the deploy depend on one repo setting: the `github-pages`
environment must allow the tag to deploy.

Current policies, under **Settings β†’ Environments β†’ github-pages β†’ Deployment
branches and tags**:

```
branch: main
tag: v*
```

Without the tag rule the deploy fails at the very last step with:

> Tag "vX.Y.Z" is not allowed to deploy to github-pages due to environment
> protection rules.

That is what happened to every release through 0.6.1, and the workaround β€”
dispatching the workflow from `main` β€” publishes `main`'s docs instead of the
release's, which is exactly what the release-only trigger exists to prevent. If
a deploy fails this way, fix the policy and re-run the workflow against the tag
rather than dispatching from `main`. Inspect the policies with:

```bash
gh api repos/<owner>/<repo>/environments/github-pages/deployment-branch-policies
```

## Automated version bump

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<groupId>dev.thiagogonzaga.thrillhousebot</groupId>
<artifactId>thrillhousebot</artifactId>
<version>0.6.2-SNAPSHOT</version>
<version>0.6.2</version>

<properties>
<compiler-plugin.version>3.15.0</compiler-plugin.version>
Expand Down
27 changes: 24 additions & 3 deletions website/scripts/archive-docs-version.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,19 @@ const versionsMetaDir = join(websiteRoot, "src/content/versions");
const versionsPath = join(websiteRoot, "versions.json");

// Keep in sync with the sidebar in astro.config.mjs (starlight-versions stores
// a copy per archived release).
// a copy per archived release). A page archived with no entry here is
// unreachable from the version's navigation, so the archive aborts on one
// rather than writing a sidebar that silently hides it. Entries whose page is
// absent from the snapshot are dropped, which is how older versions archive
// cleanly.
const SIDEBAR = [
{ label: "Home", slug: "index" },
{ label: "Getting started", slug: "getting-started" },
{ label: "Commands", slug: "commands" },
{ label: "Configuration", slug: "configuration" },
{ label: "AI providers", slug: "providers" },
{ label: "Architecture", slug: "architecture" },
{ label: "Finding feedback", slug: "feedback" },
{ label: "How it compares", slug: "comparison" },
{ label: "Contributing", slug: "contributing" },
];
Expand Down Expand Up @@ -112,8 +117,24 @@ function listMarkdownFiles(dir) {
});
}

const sources = listMarkdownFiles(docsDir);
const archivedSlugs = new Set(
sources.map((src) => relative(docsDir, src).replace(/\.mdx?$/, "")),
);

const unlisted = [...archivedSlugs].filter((s) => !SIDEBAR.some((e) => e.slug === s)).sort();
if (unlisted.length > 0) {
console.error(`Pages with no SIDEBAR entry: ${unlisted.join(", ")}`);
console.error(
"Archiving them now would leave them reachable only by direct URL. Add them to",
);
console.error("SIDEBAR in this script (and to astro.config.mjs if missing), then re-run.");
process.exit(1);
}
const sidebar = SIDEBAR.filter((e) => archivedSlugs.has(e.slug));

mkdirSync(destDocs, { recursive: true });
for (const src of listMarkdownFiles(docsDir)) {
for (const src of sources) {
const rel = relative(docsDir, src);
const out = join(destDocs, rel);
mkdirSync(dirname(out), { recursive: true });
Expand All @@ -136,7 +157,7 @@ if (existsSync(assetsDir)) {
}

mkdirSync(versionsMetaDir, { recursive: true });
writeFileSync(destVersionConfig, `${JSON.stringify({ sidebar: SIDEBAR }, null, 2)}\n`);
writeFileSync(destVersionConfig, `${JSON.stringify({ sidebar }, null, 2)}\n`);
console.log(`wrote ${relative(websiteRoot, destVersionConfig)}`);

const versions = JSON.parse(readFileSync(versionsPath, "utf8"));
Expand Down
Binary file added website/src/assets/0.6.0/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added website/src/assets/0.6.0/live-streaming.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added website/src/assets/0.6.0/pr-approval.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added website/src/assets/0.6.1/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added website/src/assets/0.6.1/live-streaming.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added website/src/assets/0.6.1/pr-approval.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading