Build metadata and tags

RFC: Custom Build Metadata for Tuist Insights

Summary

This RFC proposes adding support for custom metadata (tags and key-value pairs) to Tuist’s build insights feature. Teams will be able to attach arbitrary metadata to builds, enabling better filtering, categorization, and correlation with external systems.

Motivation

Build insights become significantly more valuable when teams can attach context specific to their workflows. Common use cases include:

  • Categorizing builds: Tag builds by feature team, project area, or build type (nightly, release candidate, PR validation)
  • Correlating with external systems: Link builds to Jira tickets, Linear issues, GitHub PRs, or internal dashboards
  • Tracking experiments: Attach A/B test identifiers, feature flag states, or configuration variants
  • Environment context: Capture CI runner labels, machine pools, or deployment targets
  • Performance baselines: Mark baseline builds for regression comparisons

Prior Art

Gradle Enterprise (Develocity)

Develocity provides three mechanisms for extending build scans:

  1. Tags — Simple labels for categorization (e.g., CI, LOCAL, dirty, release)
  2. Links — Hyperlinks to external resources (CI builds, tickets, source commits)
  3. Custom Values — Key-value pairs for metrics and computed data

Example Gradle DSL:

buildScan {
    tag("CI")
    link("CI Build", System.getenv("BUILD_URL"))
    value("Git Commit ID", commitId)
    value("Git Branch Name", branchName)
}

Gradle also provides the Common Custom User Data plugin which automatically captures common metadata like Git info, IDE vs CLI invocation, and CI-specific data. Tuist already captures similar CI metadata automatically — this RFC focuses on enabling user-defined custom metadata.

Bazel

Bazel uses the --build_metadata flag to attach key-value pairs:

bazel build //... --build_metadata=REPO_URL=https://github.com/org/repo
bazel build //... --build_metadata=BRANCH_NAME=main
bazel build //... --build_metadata=TAG_ENV=production

Metadata flows through the Build Event Protocol as BuildMetadata events, which tools like BuildBuddy consume for display and filtering.

Bazel also supports a workspace status command — a script that outputs key-value pairs, enabling dynamic metadata computation at build time.

Proposed Solution

Primary Mechanism: Environment Variables

Environment variables are the recommended approach for attaching custom metadata. This choice is driven by how build insights are integrated: the primary integration point is the post-action scheme that runs tuist inspect build after each build. Environment variables are naturally inherited by post-action scripts, making them the most practical mechanism.

# Tags (comma-separated or multiple prefixed variables)
export TUIST_BUILD_TAGS="nightly,ios-team,release-candidate"

# Or using prefixed individual tags
export TUIST_BUILD_TAG_TEAM="ios-team"
export TUIST_BUILD_TAG_TYPE="nightly"

# Custom key-value pairs
export TUIST_BUILD_METADATA_TICKET="PROJ-1234"
export TUIST_BUILD_METADATA_RUNNER="macos-14-xlarge"
export TUIST_BUILD_METADATA_JIRA="https://jira.company.com/browse/PROJ-1234"
export TUIST_BUILD_METADATA_PR="https://github.com/org/repo/pull/123"

The dashboard will automatically detect URL values in metadata and render them as clickable links.

Why environment variables?

  • Post-action compatibility: Environment variables are inherited by post-action scripts, where tuist inspect build runs
  • Works seamlessly with all CI systems: Set variables in your CI workflow and they flow through to the post-action
  • No code changes required: No need to modify schemes or project configuration
  • Easy to set conditionally: CI workflows can set different metadata based on branch, trigger, etc.
  • Familiar pattern: Developers already use environment variables for CI configuration
  • Cross-platform: Environment variables work the same way across Apple and Android/Gradle builds

Android/Gradle Support

The initial implementation will focus on Apple platforms (via tuist inspect build post-actions). However, the environment variable approach is designed to be cross-platform.

For Android/Gradle projects, custom metadata will work the same way — Gradle plugins can read environment variables via System.getenv(). When Tuist adds build insights for Gradle projects, a Tuist Gradle plugin would:

  1. Hook into Gradle build events
  2. Read TUIST_BUILD_* environment variables
  3. Send build data + custom metadata to the Tuist server

This means teams with mixed Apple/Android codebases can use the same TUIST_BUILD_* environment variables in their CI workflows, and metadata will flow to both platforms’ build insights.

Metadata Types

Type Purpose Example
Tags Simple labels for filtering and grouping nightly, release, ios-team, flaky-test-retry
Metadata Key-value pairs for structured data (URLs are auto-linked) ticket=PROJ-1234, runner=macos-14-xlarge, jira=https://...

Dashboard Integration

Build List View

  • Display tags as colored chips/badges next to each build
  • Filter builds by tags via multi-select dropdown
  • Filter by metadata via a filter control that allows specifying both key and value (e.g., key: ticket, value: PROJ-1234)

Build Detail View

  • Show tags prominently at the top (as chips/badges)
  • Display custom metadata in a dedicated “Custom Metadata” section, showing key-value pairs in a simple table format
  • Auto-detect URL values and render them as clickable links

API Changes

Create Build Run (existing endpoint extension)

{
  "build": {
    "duration": 45000,
    "status": "success",
    "scheme": "App",
    "custom_metadata": {
      "tags": ["nightly", "release-candidate"],
      "values": {
        "ticket": "PROJ-1234",
        "runner": "macos-14-xlarge",
        "jira": "https://jira.company.com/browse/PROJ-1234",
        "pr": "https://github.com/org/repo/pull/123"
      }
    }
  }
}

Alternatives Considered

1. Command-Line Flags

Adding --tag and --metadata flags to tuist inspect build:

tuist inspect build \
  --tag "nightly" \
  --metadata "ticket=PROJ-1234" \
  --metadata "jira=https://jira.company.com/browse/PROJ-1234"

Not recommended as primary mechanism because:

  • Post-action scheme limitation: The primary integration for build insights is via post-action schemes that run tuist inspect build after each build. Post-action scripts are defined in the Xcode scheme and cannot easily accept dynamic command-line arguments.
  • Requires scheme modification: To use flags, teams would need to modify their scheme’s post-action, which defeats the purpose of the seamless auto-generated scheme integration.
  • Environment variables solve the same problem: Any metadata that could be passed via flags can be set as environment variables, which are inherited by post-actions automatically.

Command-line flags could be added later for users who invoke tuist inspect build directly (outside of post-actions), but this is a minority use case.

References

I like the model.

While reading the proposal, I found myself wondering whether we need an umbrella term to group metadata (key-value pairs) and tags (single values). Or just call everything metadata, and have single or key-pair values. What prompted me thinking about it was this table that used metadata both, as an umbrella concept as an individual type of metadata:

When filtering for a particular tag, how do you plan to model this using columns and indexes?

The common umbrella term should be custom metadata or just metadata. I agree it’s confusing to then have metadata referring to also specifically to the key-value pairs. We could align with Gradle and call the key-value pairs as “values”? wdyt?

I would probably skip surfacing the values (kv pairs) as columns, although we could do it if you see value in that, such as by having a Values column where the kv pairs will be surfaced in a custom UI component. The issue is that columns should not take too much space and I can see teams having a lot of kv pairs, which is why I would maybe consider not having a column and only directly surface the kv pairs in the build detail.

When filtering, we can have a similar component like Supabase where you can specify both the key and the value:

That makes sense

Very onboard!