RFC: Bundle Size CI Check Threshold

Summary

Add bundle size threshold checks to tuist inspect bundle via GitHub Check Runs. When a bundle is uploaded and a threshold is exceeded, the server posts a check run with an action_required conclusion and an “Accept” button directly in the GitHub PR UI. Clicking “Accept” flips the check to success — no CI re-run needed and no context switching to external dashboards. This lets teams catch bundle size regressions before merge while keeping the override workflow frictionless.

Motivation

Today, Tuist supports bundle size alerting via Slack notifications configured through the dashboard. While useful for monitoring trends on tracked branches, Slack alerts notify you only after a problematic merge but don’t block problematic changes.

Teams need a way to block PRs when a bundle size regression is detected. Key use cases from real customer feedback:

  • Catch regressions before merge. Teams want to detect bundle size explosions on feature branches before they hit main, not after.
  • Enforce budgets across all branches. Rather than configuring alerts per-branch, teams want a single threshold that applies to every tuist inspect bundle invocation in CI.
  • Non-blocking visibility with override escape hatch. Sometimes a significant bundle size increase is expected and intentional. The workflow should allow reviewers to acknowledge the increase rather than permanently blocking the PR.

Prior Art

  • tuist inspect dependencies --only implicit: Exits with a non-zero status when issues are found. This is the closest existing pattern in Tuist and the one users have explicitly referenced as the desired behavior for bundle size.
  • Webpack performance hints: Webpack supports performance.maxAssetSize and performance.hints (set to "error") to fail builds when bundle sizes exceed thresholds.
  • Bundlesize / size-limit: Popular JS ecosystem tools that run in CI, compare against configured budgets, and post GitHub status checks.
  • Emerge Tools status checks: Posts GitHub status checks that fail when size thresholds are exceeded. Uses an “Action Required” → “Accept” button pattern where developers acknowledge expected increases in the dashboard rather than modifying CI configuration.
  • Android App Bundles: Google Play Console warns developers about APK size increases, but this happens post-upload rather than in CI.

Proposed Solution

Dedicated Bundle Thresholds Configuration

Introduce a new Bundles tab in the project settings, separate from the existing Notifications/Alerts system. This tab is purpose-built for configuring bundle size thresholds that control whether the GitHub check run passes or fails.

The separation from alerts is intentional: as opposed to Slack alerts, thresholds are active PR gates via GitHub check runs. Mixing them in the same UI conflates two different concerns and makes it harder for teams to reason about what will block their PRs vs. what will send a Slack message.

Bundle Size Thresholds

Each threshold is a standalone rule configured in the Bundles settings tab:

Field Description Required
Name Human-readable label (e.g., “Main app install size budget”) Yes
Metric install_size or download_size Yes
Deviation percentage Maximum allowed increase before failing (e.g., 5.0%) Yes
Baseline branch Branch to compare against (e.g., main) Yes
Bundle name Filter to a specific app bundle identifier No

Multiple thresholds can be configured per project (e.g., one for install size at 5%, another for download size at 10%, or different thresholds per app in a multi-app project).

Server-Side Evaluation and GitHub Check Run

No changes are needed to the POST /bundles API response or the tuist inspect bundle CLI command. The command behaves exactly as it does today. All threshold logic is server-side and communicated to the developer via a GitHub check run.

Threshold evaluation only happens for bundles uploaded from CI environments. Local uploads are never checked against thresholds — this prevents developers from being blocked during local iteration, where cache invalidations or experimental builds may temporarily inflate bundle sizes.

When tuist inspect bundle uploads a bundle from CI, the server:

  1. Finds the latest bundle on the configured baseline branch matching the same app bundle identifier.
  2. Compares the relevant size metric (install or download) of the uploaded bundle against that baseline.
  3. Creates a GitHub check run (named tuist/bundle-size) on the commit:
    • success conclusion if no thresholds are exceeded.
    • action_required conclusion if a threshold is exceeded, with an “Accept” action button and a link to the bundle page.

The comparison is always against a fixed baseline branch, not a rolling window. This is intentional: you want to know “how does this PR compare to main?” rather than “how does this compare to the last upload on this same branch?”

There is a single tuist/bundle-size check run per project that evaluates all configured threshold rules. If any rule is violated, the check run requires action. If multiple thresholds are exceeded, only the first violation is reported in the check run summary.

Handling Expected Increases

Sometimes a significant bundle size increase is expected and intentional (e.g., adding a new SDK, large asset migration). Since enforcement happens via GitHub check runs rather than the CLI exit code, the override flow happens directly in GitHub:

  1. When a threshold is exceeded, the server creates a check run with action_required conclusion and an “Accept” action button.
  2. The developer (or reviewer) clicks “Accept” directly in the GitHub PR checks UI — no need to leave GitHub.
  3. GitHub sends a check_run.requested_action webhook to the Tuist server.
  4. The server updates the check run conclusion to success — the PR is unblocked immediately, no CI re-run needed.

Accepting applies to all violations on that commit at once — there is no per-threshold granularity. The acceptance is scoped to a specific commit, so future commits on the same branch are still checked against the threshold.

Usage Examples

Basic setup

  1. Connect the Tuist GitHub App to your repository (required for check runs).
  2. Navigate to project settings > Bundles in the Tuist dashboard.
  3. Create a new threshold:
    • Name: “Install size budget”
    • Metric: Install size
    • Deviation: 5%
    • Baseline branch: main
  4. Add tuist inspect bundle MyApp.ipa to your CI pipeline.
  5. Add tuist/bundle-size as a required status check in your GitHub branch protection rules.

CI pipeline (GitHub Actions)

- name: Check bundle size
  run: tuist inspect bundle build/MyApp.ipa
  # Always succeeds — enforcement happens via the GitHub status check

Accepting an expected increase

If a threshold is exceeded and the increase is intentional:

  1. Developer sees the tuist/bundle-size check run with action_required status in the PR.
  2. Clicks the “Accept” button directly in the GitHub checks UI.
  3. The check run flips to success — PR is unblocked, no CI re-run needed.

Alternatives Considered

Fail tuist inspect bundle directly (non-zero exit code)

Have the CLI command itself exit non-zero when a threshold is exceeded, similar to tuist inspect dependencies --only implicit. Rejected because:

  • Once the CLI exits non-zero, the only way to unblock the PR is to re-run the entire CI job, which rebuilds everything.
  • There’s no clean “accept” workflow — the developer would need to either modify the CI pipeline (adding a skip flag, polluting the PR diff) or adjust the threshold.
  • GitHub check runs provide a better enforcement model: the server can update the check from action_required to success without re-running CI, and the developer can accept directly in the GitHub UI.

Reuse existing alert rules with a fail_command toggle

Add a boolean fail_command field to alert rules with the bundle_size category. When enabled, the server evaluates the alert rule synchronously during bundle upload and violations cause the CLI to fail.

This would avoid introducing a new settings surface, but was rejected because:

  • Alerts and CI gates serve different purposes — alerts are passive notifications, thresholds are active blockers. Bundling them together makes it harder to reason about CI behavior.
  • Alert rules have fields that don’t apply to thresholds (Slack channel, cooldown period, rolling window) and vice versa. The UI would need conditional field visibility that adds complexity.
  • A dedicated Bundles tab provides a clearer home for future bundle-related settings (e.g., absolute size budgets, per-artifact thresholds).

Client-side threshold configuration in Tuist.swift

// NOT proposed
let tuist = Tuist(inspectOptions: .init(
    bundleSizeThreshold: .init(installSize: .percentage(5), comparedTo: "main")
))

Rejected because:

  • Requires the CLI to fetch the baseline bundle size from the server anyway, duplicating server logic.
  • Splitting configuration between manifest and dashboard is confusing.
  • The server already stores bundle history; it should own the comparison logic.

Separate tuist inspect bundle --check subcommand

A dedicated subcommand that only performs the threshold check without uploading. Rejected because it would require a second server round-trip and the upload + check flow is the natural CI workflow.

Percentage-only thresholds (no absolute size budgets)

The initial version only supports percentage-based deviation thresholds. Absolute size budgets (e.g., “fail if install size exceeds 100 MB”) could be added later but are out of scope for this RFC.

Implementation References

GitHub Check Runs API

We use the Check Runs API instead of the simpler commit statuses API because check runs support the action_required conclusion with custom action buttons directly in the GitHub UI.

Creating a check run via POST /repos/{owner}/{repo}/check-runs (GitHub docs):

{
  "name": "tuist/bundle-size",
  "head_sha": "abc123...",
  "status": "completed",
  "conclusion": "action_required",
  "details_url": "https://tuist.dev/team/project/bundles/123",
  "output": {
    "title": "Bundle size threshold exceeded",
    "summary": "Install size increased by 9.68% (threshold: 5.0%)\nPrevious: 45.3 MB (main) → Current: 49.7 MB"
  },
  "actions": [
    {
      "label": "Accept",
      "description": "Accept this size increase",
      "identifier": "accept_bundle_size"
    }
  ]
}
  • conclusion: action_required: Shows as a failing check that requires developer action. When actions are provided, GitHub renders action buttons directly in the PR checks UI.
  • actions: Up to 3 buttons (max 20 char label, 40 char description, 20 char identifier). When clicked, GitHub sends a check_run.requested_action webhook event to the Tuist server with the requested_action.identifier.
  • details_url: Links to the bundle page in the Tuist dashboard for a detailed breakdown.

Updating a check run via PATCH /repos/{owner}/{repo}/check-runs/{check_run_id} — when the server receives the check_run.requested_action webhook with identifier: "accept_bundle_size", it updates the check run’s conclusion to success.

Permission: Requires the GitHub App installation token with “Checks” write permission. The Tuist GitHub App currently does not have this permission, so we’ll need to bump the app’s permissions. GitHub will prompt existing installations to approve the new permission — this is a standard flow and should be low-friction for users.

Webhook handling: The server needs to handle the check_run webhook event (action: requested_action) in the existing GitHub webhook controller (server/lib/tuist_web/controllers/webhooks/github_controller.ex). This follows the same pattern as the existing issue_comment and installation event handlers.

1 Like

Love the feature.
Also think the Server → GitHub driven architecture makes a lot of sense for this opposed to failing CI; would definitely build this as an abstraction so we can reuse it in the future.

In general think for external systems integration, server-driven bits make a lot of sense since we can own retries, error logs etc rather than it being ran on the CLI where we have significantly less insight.

Same here, and agreed with @cschmatzler’s idea of making it reusable with other metrics in the future.